# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: leon, xguo

import base64
import enum
import hashlib
import hmac
import json
import logging
import time
import urllib.parse
from typing import Union

import requests
import requests.auth

from coin.exchange.base.kr_rest.auth_base import AuthKey
from coin.exchange.bithumb.kr_rest.currency import validate_currency, validate_currency_all
from coin.exchange.util.async_http_client import AsyncHttpClient
from coin.exchange.util.order_checker import OrderAmountChecker
from coin.exchange.util.rate_limit import RateLimiter
from coin.exchange.base.kr_rest.rest_client_base import RestUpdate

BITHUMB_API_URL = "api.bithumb.com"
BITHUMB_PRIME_API_URL = "prime.bithumb.com"


# 0: entire, 1: completed purchase, 2: completed sales,
# 3: withdrawing, 4: deposit, 5: withdrawal, 9: KRW depositing
class TransactionType(enum.IntEnum):
  ENTIRE = 0  # ALL
  COMPLETED_PURCHASE = 1  # Bought
  COMPLETED_SALES = 2  # Sold
  WITHDRAWING = 3  # Withdraw in progress
  DEPOSIT = 4  # Deposit
  WITHDRAW = 5  # Withdraw
  KRW_DEPOSITING = 9  # Deposit KRW in progress


class BithumbOrderSide(enum.Enum):
  BUY = 'bid'
  SELL = 'ask'


class BithumbAuth(requests.auth.AuthBase):
  def __init__(self, key_file):
    self.key = AuthKey.from_file(key_file)
    self._logger = logging.getLogger(__name__)

  def __call__(self, req: requests.PreparedRequest):
    endpoint = urllib.parse.urlparse(req.url).path
    nonce = str(int(round(time.time() * 1000)))
    hmac_data = (endpoint + chr(0) + req.body + chr(0) + nonce).encode()
    hmh = hmac.new(self.key.secret_key_bytes, hmac_data, hashlib.sha512)
    api_sign = base64.b64encode(hmh.hexdigest().encode()).decode()

    headers = {
        'Api-Key':
            self.key.access_key,
        'Api-Sign':
            api_sign,
        'Api-Nonce':
            nonce,
        'User-Agent':
            'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11'  # noqa: E501
    }
    req.headers.update(headers)
    return req


class BithumbSyncQueryer(object):
  def __init__(self, key_file, *, api_host=None, timeout=10, proxy=None):
    self._timeout = timeout
    self._auth = BithumbAuth(key_file)
    self._api_host = "https://" + (api_host or BITHUMB_API_URL)
    assert proxy is None or isinstance(proxy, dict)
    self._proxy = proxy
    self.rate_limit = RateLimiter(8, 1.0)  # 10 per second, set to 8 to leave some room.
    self._logger = logging.getLogger(__name__)

  def auth_request(self, path, params):
    uri_array = {"endpoint": path}
    uri_array.update(params)
    url = urllib.parse.urljoin(self._api_host, path)
    response = None
    try:
      response = requests.post(url,
                               data=uri_array,
                               proxies=self._proxy,
                               auth=self._auth,
                               timeout=self._timeout)
      content = response.json()
      return content
    except requests.exceptions.RequestException as e:
      self._logger.exception('RequestException: %s', e)
      raise
    except json.JSONDecodeError as e:
      self._logger.exception('JsonDecodeException: %s\n %s', response.content, e)
      raise
    except Exception as e:
      self._logger.exception('Unknown exception, %s', e)
      raise

  def _query_nocheck(self, endpoint, params):
    req_timestamp = time.time()
    content = self.auth_request(endpoint, params)
    res_timestamp = time.time()
    if content:
      status = requests.codes.ok
    else:
      status = None
    update = RestUpdate(req_timestamp, res_timestamp, content, status)
    return update

  def query(self, path, params=None, disable_rate_limit=False):
    if not disable_rate_limit:
      while not self.rate_limit.check():
        time.sleep(0.1)
    else:
      self.rate_limit.check()
    return self._query_nocheck(path, params)


class BithumbAsyncQueryer(object):
  def __init__(self, key_file, *, api_host=None, timeout=10, max_connections=8):
    self._timeout = timeout
    self._http_client = AsyncHttpClient(max_connections=max_connections)
    self._auth = BithumbAuth(key_file)
    self._api_host = "https://" + (api_host or BITHUMB_API_URL)

  def query(self, path, params=None):
    url = urllib.parse.urljoin(self._api_host, path)
    future = self._http_client.post(url, data=params, auth=self._auth, timeout=self._timeout)
    return future


class BithumbNativePrivateClient(object):
  def __init__(self,
               *,
               key_file,
               api_host=None,
               timeout=10,
               max_connections=8,
               use_async=False,
               proxy=None):
    if proxy:
      api_host = BITHUMB_PRIME_API_URL
    if use_async:
      self._auth_queryer = BithumbAsyncQueryer(key_file,
                                               api_host=api_host,
                                               timeout=timeout,
                                               max_connections=max_connections)
    else:
      self._auth_queryer = BithumbSyncQueryer(key_file,
                                              api_host=api_host,
                                              timeout=timeout,
                                              proxy=proxy)
    self._order_amount_checker = OrderAmountChecker("KRW", 500, 50000000)

  def _query(self, path, params=None):
    if params:
      params = {key: value for key, value in params.items()}
    return self._auth_queryer.query(path, params=params)

  def query_info_account(self, order_currency: str, payment_currency: Union[None, str] = None):
    validate_currency_all(order_currency)
    url = "/info/account"
    payload = {'order_currency': order_currency}
    if payment_currency is not None:
      validate_currency(payment_currency)
      payload['payment_currency'] = payment_currency
    update = self._query(url, payload)
    return update

  def query_info_balance(self, currency='ALL'):
    validate_currency_all(currency)
    url = "/info/balance"
    payload = {'currency': currency}
    update = self._query(url, payload)
    return update

  def query_info_wallet_address(self, currency):
    raise NotImplementedError()

  def query_info_ticker(self, currency):
    raise NotImplementedError()

  def query_info_orders(self,
                        order_currency: str,
                        payment_currency: Union[None, str] = None,
                        order_id: Union[None, str] = None,
                        order_side: Union[None, BithumbOrderSide] = None,
                        count: Union[None, int] = None,
                        after: Union[None, int] = None):
    validate_currency_all(order_currency)
    if after is None:
      after = int(time.time() - 600)
    if count is not None:
      assert 1 <= count <= 1000, count

    url = "/info/orders"
    payload = {
        'order_currency': order_currency,
        'count': count,
        'after': after,
        'order_id': order_id,
        'type': order_side,
    }
    if payment_currency is not None:
      validate_currency(payment_currency)
      payload['payment_currency'] = payment_currency
    update = self._query(url, payload)
    return update

  def query_info_order_details(
      self,
      order_currency: str,
      order_id: str,
      payment_currency: Union[None, str] = None,
  ):
    validate_currency(order_currency)
    url = '/info/order_detail'
    payload = {'order_id': order_id, 'order_currency': order_currency}
    if payment_currency is not None:
      validate_currency(payment_currency)
      payload['payment_currency'] = payment_currency
    update = self._query(url, payload)
    return update

  def query_info_user_transactions(
      self,
      *,
      order_currency: str,
      payment_currency: Union[None, str] = None,
      offset=None,
      count=None,
      search: Union[None, TransactionType] = None,
  ):
    validate_currency_all(order_currency)
    if count is not None:
      assert 1 <= count <= 50, count
    if search is not None:
      search = search.value

    url = "/info/user_transactions"
    payload = {
        'order_currency': order_currency,
        'offset': offset,
        'count': count,
        'searchGb': search,
    }
    if payment_currency is not None:
      validate_currency(payment_currency)
      payload['payment_currency'] = payment_currency
    update = self._query(url, payload)
    return update

  def query_trade_place(
      self,
      order_currency: str,
      payment_currency: str,
      price,
      qty,
      order_side: BithumbOrderSide,
  ):
    validate_currency(order_currency)
    validate_currency(payment_currency)
    self._order_amount_checker.check_amount("KRW", price, qty)

    url = "/trade/place"
    payload = {
        'order_currency': order_currency,
        'payment_currency': payment_currency,
        'price': price,
        'units': qty,
        'type': order_side.value,
    }
    update = self._query(url, payload)
    return update

  def query_trade_cancel(self,
                         order_currency: str,
                         payment_currency: str,
                         order_id: str,
                         order_side: BithumbOrderSide):
    validate_currency(order_currency)
    validate_currency(payment_currency)
    assert isinstance(order_id, str), type(order_id)

    url = "/trade/cancel"
    payload = {
        'order_currency': order_currency,
        'payment_currency': payment_currency,
        'order_id': order_id,
        'type': order_side.value
    }
    update = self._query(url, payload)
    return update

  def query_trade_coin_withdrawal(self, currency):
    raise NotImplementedError()

  def query_trade_krw_withdrawal(self, currency):
    raise NotImplementedError()

  def query_trade_krw_deposit(self, currency):
    raise NotImplementedError()
